package cn.coisini.service.impl;

import cn.coisini.mapper.AsyncLoginLogMapper;
import cn.coisini.mapper.UserMapper;
import cn.coisini.model.common.constants.RedisConstants;
import cn.coisini.model.common.constants.SecurityConstants;
import cn.coisini.model.common.constants.message.AdminMessageConstants;
import cn.coisini.model.common.dtos.Result;
import cn.coisini.model.common.enums.ResultEnum;
import cn.coisini.model.common.enums.SmsType;
import cn.coisini.model.common.exception.CustomAuthenticationException;
import cn.coisini.model.common.log.service.AsyncLoginLogService;
import cn.coisini.model.common.mail.MailService;
import cn.coisini.model.common.sms.SmsService;
import cn.coisini.model.common.sms.property.AliyunSmsProperties;
import cn.coisini.model.dto.UserDto;
import cn.coisini.model.pojo.AsyncLoginLog;
import cn.coisini.model.pojo.User;
import cn.coisini.model.vo.RouterVo;
import cn.coisini.security.auth.AuthUser;
import cn.coisini.service.AuthService;
import cn.coisini.service.MenuService;
import cn.coisini.utils.*;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.imageio.ImageIO;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import static cn.coisini.model.common.constants.AdminConstants.*;
import static cn.coisini.model.common.constants.SecurityConstants.CAPTCHA_PREFIX;
import static cn.coisini.utils.JwtUtil.getUserIdFromToken;
import static cn.coisini.utils.JwtUtil.getUsernameFromToken;

/**
 * @author xiangshaw
 * Description: 认证服务
 */
@Log4j2
@Service
@RequiredArgsConstructor
public class AuthServiceImpl implements AuthService {
    // 身份认证
    private final AuthenticationManager authenticationManager;
    private final StringRedisTemplate stringRedisTemplate;
    private final MenuService menuService;
    private final UserMapper userMapper;
    private final AsyncLoginLogMapper asyncLoginLogMapper;
    private final AsyncLoginLogService asyncLoginLogService;
    private final KafkaTemplate<String, String> kafkaTemplate;
    private final MailService mailService;
    private final AliyunSmsProperties aliyunSmsProperties;
    private final SmsService smsService;

    // 访问地址
    @Value("${file.upload.local.url}")
    private String url;
    // 令牌过期时间
    @Value("${token.time}")
    private Integer tokenTimeOut;

    // 用户注册
    @Override
    public Result<ResultEnum> register(UserDto userDto) {
        // 1.校验验证码
        registerValidateCaptcha(userDto);
        // 2.检查用户名是否已经注册
        if (checkUserRegistered(userDto.getUsername())) {
            return Result.error(ResultEnum.DATA_EXIST, "该用户名已经注册");
        }
        // 3.密码加密
        userDto.setPassword(new BCryptPasswordEncoder().encode(userDto.getPassword()));
        // 4.添加用户
        User user = createUser(userDto);
        if (userMapper.insert(user) > FALSE) {
            return Result.ok(ResultEnum.SUCCESS, "注册成功");
        }
        return Result.error(ResultEnum.FAIL, "注册失败");
    }

    private void registerValidateCaptcha(UserDto userDto) {
        String checkCode = stringRedisTemplate.opsForValue().get(CAPTCHA_PREFIX + userDto.getUuid());
        if (checkCode == null || CharSequenceUtil.isBlank(checkCode)) {
            throw new CustomAuthenticationException(ResultEnum.SECURITY_CODE, ResultEnum.SECURITY_CODE.getMessage());
        }
        if (!checkCode.equals(userDto.getCaptcha())) {
            throw new CustomAuthenticationException(ResultEnum.SECURITY_CODE_ERROR, ResultEnum.SECURITY_CODE_ERROR.getMessage());
        }
    }

    // 检查用户名是否已经注册
    private boolean checkUserRegistered(String username) {
        return userMapper.selectCount(Wrappers.<User>lambdaQuery().eq(User::getUsername, username)) > ACCOUNT_NORMAL;
    }

    // 创建用户
    private User createUser(UserDto userDto) {
        User user = new User();
        user.setId(String.valueOf(new IdWorker().nextId()));
        user.setUsername(userDto.getUsername());
        user.setPassword(userDto.getPassword());
        user.setNickName(userDto.getNickName());
        return user;
    }

    /**
     * 登录认证
     */
    @Override
    public Result<String> authenticate(AuthUser authUser, HttpServletRequest request) {
        // 1.检验验证码
        validateCaptcha(authUser);
        // 2.用户身份认证
        try {
            authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(authUser.getUsername(), authUser.getPassword()));
        } catch (AuthenticationException ex) {
            throw new CustomAuthenticationException(ResultEnum.AUTHENTICATION_FAILED, "用户名或密码错误");
        }
        // 3.查询用户并生成 JWT 令牌
        return Result.ok(generateAuthToken(authUser, request), ResultEnum.LOGIN_SUCCESS.getMessage());
    }

    /**
     * 检测验证码
     */
    private void validateCaptcha(AuthUser authUser) {
        String checkCode = stringRedisTemplate.opsForValue().get(CAPTCHA_PREFIX + authUser.getUuid());
        if (checkCode == null || CharSequenceUtil.isBlank(checkCode)) {
            throw new CustomAuthenticationException(ResultEnum.SECURITY_CODE, "验证码不存在或已过期");
        }
        if (!checkCode.equals(authUser.getCaptcha())) {
            throw new CustomAuthenticationException(ResultEnum.SECURITY_CODE_ERROR, "验证码错误");
        }
    }

    /**
     * 生成 JWT 令牌
     */
    private String generateAuthToken(AuthUser authUser, HttpServletRequest request) {
        QueryWrapper<User> queryWrapper = new QueryWrapper<User>().eq("user_name", authUser.getUsername());
        User user = userMapper.selectOne(queryWrapper);
        if (user == null) {
            throw new CustomAuthenticationException(ResultEnum.DATA_NOT_FOUND_BY_USERNAME, "用户不存在");
        }

        // 更新登录时间
        HashMap<String, Object> map = new HashMap<>();
        map.put("authUser", authUser);
        map.put("user", user);
        // 异步发布消息 更新登录时间
        log.info("用户id:{},username:{}登录成功，发送异步消息更新登录时间,map信息:{}", user.getId(), authUser.getUsername(), JSON.toJSONString(map));
        kafkaTemplate.send(AdminMessageConstants.ADMIN_UPDATE_LOGIN_TIME, JSON.toJSONString(map));

        // 记录登录日志
        recordLoginLog(user, request);

        Map<String, Object> claims = Map.of("id", user.getId(), USERNAME, authUser.getUsername());
        String token = JwtUtil.getToken(claims);
        log.info("用户id:{},username:{}登录成功，生成 JWT 令牌 {}", user.getId(), authUser.getUsername(), token);
        stringRedisTemplate.opsForValue().set(JSONUtil.toJsonStr(SecurityConstants.LOGIN_TOKEN + token), token, tokenTimeOut, TimeUnit.SECONDS);
        return token;
    }

    /**
     * 记录登录日志
     */
    private void recordLoginLog(User user, HttpServletRequest request) {
        try {
            // 创建并保存登录日志对象
            AsyncLoginLog asyncLoginLog = createAndSaveLoginLog(user, request);
            // 检查并发送登录通知邮件（若本次登录 IP 与上次不同）
            sendLoginNotificationIfIpChanged(user, asyncLoginLog);
        } catch (Exception e) {
            handleLoginLogException(user, request, e);
        }
    }

    /**
     * 检查并发送登录通知邮件（若本次登录 IP 与上次不同）
     *
     * @param user          用户
     * @param asyncLoginLog 登录日志
     */
    private void sendLoginNotificationIfIpChanged(User user, AsyncLoginLog asyncLoginLog) {
        AsyncLoginLog lastLoginLog = asyncLoginLogService.getLastLoginLogByUserId(user.getId());
        boolean isNewLoginIp = lastLoginLog == null || !lastLoginLog.getLoginIp().equals(asyncLoginLog.getLoginIp());
        if (isNewLoginIp) {
            Map<String, Object> messageData = Map.of("asyncLoginLog", asyncLoginLog, "user", user);
            // 异步发布消息给邮箱，提示用户非常用地址登录成功
            kafkaTemplate.send(AdminMessageConstants.ADMIN_SEND_LOGIN_MAIL, JSON.toJSONString(messageData));
        }
    }

    /**
     * 创建并保持登录日志对象
     */
    private AsyncLoginLog createAndSaveLoginLog(User user, HttpServletRequest request) {
        String ip = IpUtils.getIp(request);
        String ipSource = IpUtils.getIp2region(ip);
        String cityInfo = IpUtils.getCityInfo(ip);
        AsyncLoginLog asyncLoginLog = new AsyncLoginLog();
        asyncLoginLog.setUserId(user.getId());
        asyncLoginLog.setUsername(user.getUsername());
        asyncLoginLog.setStatus(false);
        asyncLoginLog.setLoginIp(ip);
        asyncLoginLog.setLoginIpSource(ipSource);
        asyncLoginLog.setLoginIpCity(cityInfo);
        asyncLoginLog.setMsg("登录成功");
        // 记录登录日志
        log.info("登录日志记录成功: 用户 {} 登录，IP: {}, 来源: {}, 城市: {}",
                user.getUsername(), asyncLoginLog.getLoginIp(),
                asyncLoginLog.getLoginIpSource(), asyncLoginLog.getLoginIpCity());
        // 保存登录日志
        saveLoginLog(asyncLoginLog);
        return asyncLoginLog;
    }

    /**
     * 处理记录登录日志时的错误
     */
    private void handleLoginLogException(User user, HttpServletRequest request, Exception e) {
        log.error("记录登录日志时发生异常: 用户 {} 登录， IP: {}, 异常信息: {}",
                user.getUsername(), IpUtils.getIp(request), e.getMessage(), e);
        String username = user.getUsername();
        if (username == null) {
            username = UNKNOWN_USER;
        }
        createErrorLoginLog(username, request, e);

    }

    /**
     * 创建错误登录日志对象
     */
    private void createErrorLoginLog(String username, HttpServletRequest request, Exception e) {
        String ip = IpUtils.getIp(request);
        String ipSource = IpUtils.getIp2region(ip);
        String cityInfo = IpUtils.getCityInfo(ip);
        AsyncLoginLog asyncLoginLog = new AsyncLoginLog();
        asyncLoginLog.setUsername(username);
        asyncLoginLog.setStatus(false);
        asyncLoginLog.setLoginIp(ip);
        asyncLoginLog.setLoginIpSource(ipSource);
        asyncLoginLog.setLoginIpCity(cityInfo);
        asyncLoginLog.setMsg("登录出错: " + e.getMessage());
        saveLoginLog(asyncLoginLog);
    }

    /**
     * 保存日志
     */
    private void saveLoginLog(AsyncLoginLog loginLog) {
        asyncLoginLogMapper.insert(loginLog);
    }

    @Override
    public Result<ResultEnum> sendSms(String mode, SmsType smsType) {
        // 1. 随机生成6位数验证码
        String code = RandomUtil.randomNumbers(6);
        // 获取验证码前缀
        String codePrefix = getCodePrefix(smsType);
        // 2. 发送验证码
        try {
            switch (smsType) {
                case EMAIL:
                    // 发送邮件验证码
                    mailService.sendVerificationCode(mode, code);
                    break;
                case MOBILE:
                    // 获取阿里云短信模板
                    String templateCode = aliyunSmsProperties.getTemplateCodes().get("login");
                    if (templateCode == null) {
                        log.error("获取阿里云短信模板失败，smsType={}, mode={}", smsType, mode);
                        throw new CustomAuthenticationException(ResultEnum.DATA_INVALID_PARAMETER, "短信模板不存在");
                    }
                    // 发送短信验证码
                    smsService.sendSms(mode, templateCode, buildTemplateParam(code));
                    break;
                default:
                    log.error("不支持的验证码发送方式，smsType={}, mode={}", smsType, mode);
                    throw new CustomAuthenticationException(ResultEnum.DATA_INVALID_PARAMETER, "参数异常");
            }
        } catch (Exception e) {
            log.error("验证码发送失败，smsType={}, mode={}, 错误信息={}", smsType, mode, e.getMessage());
            return Result.error(ResultEnum.FAIL, "验证码发送失败");
        }
        // 3. 存储验证码
        storeCodeInRedis(codePrefix, mode, code);
        return Result.ok(ResultEnum.SUCCESS);
    }

    /**
     * 获取验证码前缀
     *
     * @param smsType 短信或邮件类型
     * @return 验证码前缀
     */
    private String getCodePrefix(SmsType smsType) {
        return switch (smsType) {
            case EMAIL -> RedisConstants.EMAIL_CODE_PREFIX;
            case MOBILE -> RedisConstants.MOBILE_CODE_PREFIX;
            default -> throw new CustomAuthenticationException(ResultEnum.DATA_INVALID_PARAMETER, "不支持的验证码类型");
        };
    }

    /**
     * 短信模板参数
     *
     * @param code 生成的验证码
     * @return 短信模板参数的JSON字符串
     */
    private String buildTemplateParam(String code) {
        return "{\"code\":\"" + code + "\"}";
    }

    /**
     * 将验证码存储到 Redis
     *
     * @param codePrefix 验证码的前缀
     * @param mode       接收验证码的邮箱或手机号
     * @param code       生成的验证码
     */
    private void storeCodeInRedis(String codePrefix, String mode, String code) {
        // 存储验证码到 Redis，设置过期时间为3分钟
        stringRedisTemplate.opsForValue().set(codePrefix + mode, code, 3, TimeUnit.MINUTES);
        log.info("验证码已存储，codePrefix={}, mode={}, code={}", codePrefix, mode, code);
    }


    // 获取用户信息
    @Override
    public Result<Map<String, Object>> getUserInfo() {
        String userId = getUserIdFromToken();
        String username = getUsernameFromToken();
        // 1.检查参数
        if (userId == null && StringUtils.isEmpty(username)) {
            throw new CustomAuthenticationException(ResultEnum.DATA_INVALID_PARAMETER, "参数不能为空");
        }
        // 2.根据用户id查询菜单权限值
        List<RouterVo> routerVoList = menuService.findUserMenuList(userId);
        HashMap<String, Object> map = new HashMap<>();
        // 3.根据用户id查询按钮权限值
        List<String> permsList = menuService.findUserButtonList(userId);
        // 根据用户id查询信息
        String avatar = userMapper.selectById(userId).getAvatar();
        map.put("userId", userId);
        map.put("username", username);
        map.put("avatar", url + avatar);
        // 菜单权限数据
        map.put("routers", routerVoList);
        // 按钮权限数据
        map.put("buttons", permsList);
        return Result.ok(map);
    }

    @Override
    public Result<Map<String, Object>> captcha() throws IOException {
        // 生成唯一识别码
        String uuid = IdUtil.simpleUUID();
        CaptchaUtil.Captcha captcha = CaptchaUtil.generateCaptcha();
        // 验证码存入redis中
        stringRedisTemplate.opsForValue().set(CAPTCHA_PREFIX + uuid, captcha.code(), SecurityConstants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
        // 将图像转换为Base64编码字符串
        ByteArrayOutputStream fos = new ByteArrayOutputStream();
        ImageIO.write(captcha.image(), "jpeg", fos);
        String codeImage = Base64.getEncoder().encodeToString(fos.toByteArray());
        // 返回验证码和uuid
        Map<String, Object> map = new HashMap<>();
        map.put("captcha", codeImage);
        map.put("uuid", uuid);
        return Result.ok(map);
    }

    // 用户退出登录
    @Override
    public Result<ResultEnum> logout() {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes == null) {
            return Result.error(ResultEnum.DATA_INVALID_PARAMETER);
        }
        // 获取token
        HttpServletRequest request = attributes.getRequest();
        String bearerToken = request.getHeader("Authorization");
        // 判断token是否为空，并且以Coisini 开头
        if (org.springframework.util.StringUtils.hasText(bearerToken) && bearerToken.startsWith(SecurityConstants.TOKEN_PREFIX)) {
            String token = bearerToken.substring(SecurityConstants.TOKEN_PREFIX.length());
            // 检查token是否有效
            if (JwtUtil.tokenWhetherExpired(token, JwtUtil.getTokenSigningKey())) {
                Result<ResultEnum> result = new Result<>();
                result.setCode(ResultEnum.SUCCESS.getCode());
                result.setMessage(ResultEnum.LOGOUT_SUCCESS.getMessage());
                return result;
            }
            // 获取token剩余有效时间
            long remainingTime = JwtUtil.getTokenRemainingTime(token);
            // 如果剩余时间小于等于固定的超时时间，则加入黑名单
            if (remainingTime > 0) {
                // 获取token有效期，转换为毫秒计算
                long l = tokenTimeOut * 1000;
                // 加入黑名单
                stringRedisTemplate.opsForValue().set(SecurityConstants.BLACKLIST_TOKEN + token, token, l, TimeUnit.MILLISECONDS);
            }
        }
        // 清除线程变量
        ThreadLocalUtil.remove();
        // 清空Spring Security上下文
        SecurityContextHolder.clearContext();
        return Result.ok(ResultEnum.LOGOUT_SUCCESS);
    }

    @Override
    public Result<String> hello() {
        return Result.ok("are you ok");
    }
}
